1. Canceling a Task
In .NET, a cancellation
request does not forcibly end a task. Instead, tasks use a cooperative
cancellation model. This means that a running task must poll for the
existence of a cancellation request at appropriate intervals and then
shut itself down by calling back into the library.
Note:
Tasks in .NET use a cooperative cancellation model.
The .NET Framework uses
separate types. One allows a program request cancellation, and the
other checks for cancellation requests. Instances of the CancellationTokenSource class are used to request cancellation, while CancellationToken values indicate whether cancellation has been requested. Here’s an example:
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Task myTask = Task.Factory.StartNew(() =>
{
for (...)
{
token.ThrowIfCancellationRequested();
// Body of for loop.
}
}, token);
// ... elsewhere ...
cts.Cancel();
The CancellationTokenSource object contains a cancellation token. This token is used in two places. It’s passed as an argument to the StartNew factory method, and its ThrowIfCancellationRequested
method is invoked to detect and process a cancellation request.
Although the example doesn’t show it, you can also read the
cancellation token’s IsCancellationRequested
property to test for a cancellation request. For example, you would do
this if you need to perform local cleanup actions for a task that’s in
the process of being canceled.
If the cancellation token indicates that a cancellation has been requested, the ThrowIfCancellationRequested method creates an OperationCanceledException
instance and passes in the cancellation token. It then throws the
exception. This exception is the signal that notifies the .NET
Framework that the task has been canceled; therefore, the OperationCanceledException
should not be handled by user code within the task (it is often
handled, however, outside of the task that was canceled). If you follow
the steps that have been described in this section, the task will be
stopped and its Status property will be set to the enumerated value TaskStatus.Canceled.
To ensure that a running task transitions to status Canceled, a cancellation token must have been given as an argument to the StartNew method and an unhandled OperationCanceledException exception that contains that same cancellation token must be thrown by the task.
Note:
There are several requirements that must be met before a task will transition to status Canceled.
Checking for cancellation
within a loop with many iterations that each perform a small amount of
work can negatively affect your application’s performance, but checking
only infrequently for cancellation can introduce unacceptable latency
in your application’s response to cancellation requests. For example,
in an interactive GUI-based application, checking for cancellation more
than once per second is probably a good idea. An application that runs
in the background could poll for cancellation much less frequently,
perhaps every two to ten seconds. Profiling your application can give
you performance data that you can use when determining the best places
to test for cancellation requests in your code. You need to understand
which code locations will allow you to poll for cancellation at evenly
spaced intervals of your choosing, and profiling can help you do this.
Note:
Whether or not a task gets the status Canceled
can be important. If you don’t do this correctly, the process might be
unexpectedly terminated when the improperly canceled task is garbage
collected. A typical mistake is forgetting to pass the cancellation
token to the StartNew method. Another typical mistake is forgetting to throw the OperationCanceledException that contains the required cancellation token.
If a cancellation token is passed to the StartNew
method and cancellation is signaled on that token before the new task
begins to execute, the new task’s status will transition to Canceled without ever running.
For a little fine print about the behavior of cancellation.
2. Handling Exceptions
If the execution of a Task object’s delegate throws an unhandled exception, the task terminates and its Status property is set to the enumerated value TaskStatus.Faulted. The unhandled exception is temporarily unobserved. This means that the Task Parallel
Library (TPL) catches the exception and records its details in internal
data structures associated with the task object. Recovering a deferred
exception and embedding it in a new exception is known as “observing an
unhandled task exception.” In many cases, unhandled task exceptions
will be observed in a different thread than the one that executed the
task.
2.1. Ways to Observe an Unhandled Task Exception
There are several ways to observe an unhandled task exception.
Invoking the faulted task’s Wait method causes the task’s unhandled exception to be observed. The exception is also thrown in the calling context of the Wait method. The Task class’s static WaitAll method allows you to observe the unhandled exceptions of more than one task with a single method invocation. The Parallel. Invoke method includes an implicit call to WaitAll. Exceptions from all of the tasks are grouped together in an AggregateException object and thrown in the calling context of the WaitAll or Wait method.
Getting the Exception
property of a faulted task causes the task’s unhandled exception to be
observed. The property returns the aggregate exception object. Getting
the value does not automatically cause the exception to be thrown;
however, the exception is considered to have been observed when you get
the value of the Exception property. Use the Exception property instead of the Wait method or WaitAll method when you want to examine the unhandled exception but do not want it to be rethrown in the current context.
Note:
You must take
care to observe the unhandled exceptions of each task. If you don’t do
this, .NET’s exception escalation policy can terminate your process
when the task is garbage collected.
Special handling
occurs if a faulted task’s unhandled exceptions are not observed by the
time the task object is garbage-collected.
2.2. Aggregate Exceptions
In some cases, such as when
you’re waiting for more than one task, there can be multiple unhandled
exceptions that need to be observed. For this reason, the runtime
collects the unhandled task exceptions and wraps them in an AggregateException
instance. TPL throws the aggregate exception in the context of the
observer (that is, in the waiting thread). You can catch the aggregate
exception and examine its InnerExceptions
property to handle the original exceptions individually. Even if the
unhandled exception from just one task is observed, it’s still wrapped
in an AggregateException.
Throwing an aggregate exception preserves the inner exception’s stack
trace, which would otherwise be overwritten if the exception were
rethrown in the current context.
Note:
The Task Parallel Library uses AggegrateException objects to propagate exceptions.
Like all exceptions that are raised inside of a task, an Operation-CanceledException instance that’s created by canceling a task will become an inner exception of an AggregateException thrown by the task in the context of the thread that calls the task’s Wait method.
2.3. The Handle Method
The AggregateException class has several helper methods to make handling inner exceptions easier. The Handle method of the AggregateException class invokes a user-provided delegate method for each of the inner exceptions of an aggregate exception. The delegate should return true if it has handled the inner exception and false if it has not handled the inner exception. Here’s an example.
Note:
Use the Handle method to iterate through inner exceptions.
try
{
Task t = Task.Factory.StartNew( ... );
// ...
t.Wait();
}
catch (AggregateException ae)
{
ae.Handle(e =>
{
if (e is MyException)
{
// ... handle exception ...
return true;
}
else
{
return false;
}
});
}
When the Handle
method invokes the user-provided delegate for each inner exception, it
keeps track of the results of the invocation. After it processes all
inner exceptions, it checks to see if the results for one or more of
the inner exceptions were false, which indicates that they have not been handled. If there are any unhandled exceptions, the Handle method creates and throws a new aggregate exception that contains the unhandled ones as inner exceptions.
If the Handle method doesn’t meet your needs, you can write your own code that iterates through the InnerExceptions property of an AggregateException object.
2.4. The Flatten Method
The Flatten method of the AggregateException class is useful when tasks are nested within other tasks. In this case, it’s possible that an aggregate exception can contain other aggregate exceptions as inner exceptions. The Flatten
method produces a new exception object that merges the inner exceptions
of all nested aggregate exceptions into the inner exceptions of the
top-level aggregate exception. In other words, it converts a tree of
inner exceptions into a flat sequence of inner exceptions. It’s typical
to use the Flatten and Handle methods together. This is shown in the following example.
Note:
The Flatten method transforms a tree of exceptions into a sequence.
try
{
Task t1 = Task.Factory.StartNew(() =>
{
Task t2 = Task.Factory.StartNew(() =>
{
// ...
throw new MyException();
});
// ...
t2.Wait();
});
// ...
t1.Wait();
}
catch (AggregateException ae)
{
ae.Flatten().Handle(e =>
{
if (e is MyException)
{
// ... handle exception ...
return true;
}
else
{
return false;
}
});
}